// TODO: CODE REVIEW & CLEANUP!
//
// MusicPlayerDriver.cs
//
// Implements HomeUX.Components.MusicPlayerDriver.
//
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.ServiceModel;
using System.Xml;
using HomeUX.DriverModel;
using HomeUX.MusicServer;
using HomeUX.Networking;
using HomeUX.Synchronization;
using HomeUX.Utilities;

namespace HomeUX.Components
{

    /* TODO: format:
 
       SyncPath schema:
        -- "<device>" (bool, read-only) = true if music server is available, null if not
        -- "<device>\LibraryChangeCount" (int, read-only) = number of times that the library has changed;
           monitor this to see when the contents of the library change
        -- "<device>\QueueChangeCount" (int, read-only) = number of times that the queue has changed;
           monitor this to see when the contents of the queue change
        -- "<device>\QueueSummary" (string, read-only) = "<n>;<t>" where <n> is the number of tracks queued to be
           played after the current track, and <t> is the integer number of seconds in those
           tracks
        -- "<device>\QueueSaveState" (string, read-only) = "<save-state>:<playlist-name>":
             -- "Loaded:<playlist-name>" = queue was loaded from playlist and was not modified since then
             -- "LoadedModified:<playlist-name>" = queue was loaded from playlist and was modified since then
             -- "Saved:<playlist-name>" = queue was saved to playlist and was not modified since then
             -- "SavedModified:<playlist-name>" = queue was saved to playlist and was modified since then
             -- "NewEmpty:" = queue is new (not associated with a playlist) and is empty
             -- "NewNotEmpty:" = queue is new (not associated with a playlist) and is not empty
        -- "<device>\IsLoopMode" (bool, read/write) = true if we're in loop (repeat) mode, i.e. start queue over when done
        -- "<device>\IsShuffleMode" (bool, read/write) = true if we're in shuffle mode, i.e. play queue in random order
        -- "<device>\PlaybackRate" (double, read/write) = playback rate (1.0 = normal)
    */
/// <summary>
/// Implements a HomeUX driver that controls a local or remote music player.
/// </summary>
///
public class MusicPlayerDriver : Driver, IMusicWcfServiceCallback
{
    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Private Fields
    //

#if !SILVERLIGHT
    /// <summary>
    /// A <n>DuplexChannelFactory</n> used for creating instances of <r>IMusicWcfService</r> to
	/// assign to <r>_musicServer</r> (each time we attempt to connect to the music server).
    /// </summary>
	DuplexChannelFactory<IMusicWcfService> _musicServerChannelFactory;
#endif

    /// <summary>
    /// The music server specification string that defines the real or simulated music server to
    /// connect to.
    /// </summary>
    string _musicServerSpec;

    /// <summary>
    /// The object that's simulating the music server (by implementing both
    /// <r>IMusicServerSimulator</r> and <r>IMusicWcfService</r>), or <n>null</n> if we're
    /// connecting to a real music server via WCF.
    /// </summary>
    IMusicServerSimulator _simulator;

    /// <summary>
    /// The proxy used to communicate with the music server (real or simulated).  <n>null</n>
	/// when the music server is not available.
    /// </summary>
    IMusicWcfService _musicServer;

    /// <summary>
    /// The last-reported status of the music server, or <n>null</n> if we're not connected to the
    /// music server.
    /// </summary>
    MusicServiceDetailedStatus _status;

    /// <summary>
    /// While this driver is enabled, <r>_checkAvailabilityWorkItem</r> is the current
    /// <r>WorkItem</r> used to poll the music server to see if it's up &amp; running.
    /// If <r>_musicServerIsAvailable</r> is <n>false</n>, the poll is performed frequently to see
    /// if the music server is available yet; if <n>true</n>, the poll is less frequent, to see if
    /// the music server has gone down.
    /// </summary>
    WorkItem _checkAvailabilityWorkItem;

    /// <summary>
    /// While this driver is running, this number is incremented each time the music server tells
    /// us that the library has changed.
    /// </summary>
    int _libraryChangeCount = 0;

    /// <summary>
    /// While this driver is running, this number is incremented each time the music server tells
    /// us that the queue has changed.
    /// </summary>
    int _queueChangeCount = 0;

    QueueSaveState _queueSaveState;

    /// <summary>
    /// See QueueSaveState.
    /// </summary>
    string _queueAssociatedPlaylistName;

    bool MusicServerIsAvailable
    {
        get
        {
            return _status != null;
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // Device Class Overrides
    //

    protected override void OnInitialize()
    {
        const string attributeName = "MusicServer";
        _musicServerSpec = Configuration.GetRequiredAttribute(attributeName);
        if (_musicServerSpec.Contains(';'))
        {
            // <_musicServerSpec> is the construction specification for a DLL -- set <_simulator>
            // to a new instance of that simulator
            IMusicWcfService service = Util.CreateInstance<IMusicWcfService>(_musicServerSpec);
            _simulator = service as IMusicServerSimulator;
            if (_simulator == null)
            {
                throw new ConfigurationException(Configuration,
                    "Library does not implement IMusicServerSimulator: {0}",
                    _musicServerSpec);
            }
            _simulator.Initialize(WorkQueue, this);
        }
        else
        {
            // <_musicServerSpec> is the "<server-name>:<port-number>" of a real HomeUX Music
            // Server -- set <_musicServerChannelFactory> to an object that can create a channel
            // that connects to the music server

#if !SILVERLIGHT
            // parse <_musicServerSpec> into <serverName> and <portNumber>
            string[] parts = _musicServerSpec.Split(':');
            string serverName;
            int portNumber;
            if ((parts.Length != 2) ||
                ((serverName = parts[0].Trim()).Length == 0) ||
                !Util.TryXmlConvert(parts[1], out portNumber) ||
                (portNumber <= 0) || (portNumber >= UInt16.MaxValue))
            {
                throw new ConfigurationException(Configuration,
                    "Invalid \"{0}\" attribute: Expecting \"<server-name>:<port-number>\", got \"{1}\"",
                    attributeName, _musicServerSpec);
            }

            // set <uri> to the URI of the music server, e.g.
            // "net.tcp://localhost:3535/HomeUX.MusicServer"
            var uri = new UriBuilder();
            uri.Scheme = "net.tcp";
            uri.Host = serverName;
            uri.Port = portNumber;
            uri.Path = "HomeUX.MusicServer";

            // initialize <_musicServerChannelFactory>
            var binding = new NetTcpBinding()
            {
                OpenTimeout = TimeSpan.FromMinutes(1),
                CloseTimeout = TimeSpan.FromMinutes(1),
                SendTimeout = TimeSpan.FromMinutes(1),
                ReceiveTimeout = TimeSpan.FromMinutes(10),
                MaxReceivedMessageSize = 999999999,
                ReaderQuotas = new XmlDictionaryReaderQuotas()
                {
                    MaxArrayLength = 999999,
                    MaxBytesPerRead = 999999999
                }
            };
            var endpointAddress = new EndpointAddress(uri.Uri);
            _musicServerChannelFactory = new DuplexChannelFactory<IMusicWcfService>(this, binding,
                endpointAddress);
#else
            throw new ConfigurationException(Configuration,
                "MusicPlayerDriver only supports connecting to a simulated HomeUX Music Server when running under Silverlight");
#endif
        }
    }

    IMusicWcfService CreateChannelToMusicServer()
    {
        if (_simulator != null)
            return (IMusicWcfService)_simulator;
        else
        {
#if !SILVERLIGHT
            return _musicServerChannelFactory.CreateChannel();
#else
            throw new InvalidOperationException("Internal error");
#endif
        }
    }

    protected override void OnEnable()
    {
        EnableCompleted(true);
        StartPollingForAvailability();
    }

    protected override void OnDisable()
    {
        StopPollingForAvailability();
        DisconnectFromMusicServer();
        DisableCompleted();
    }

    PollForAvailabilityStateMachine _pollForAvailabilityStateMachine;

    void StartPollingForAvailability()
    {
        _pollForAvailabilityStateMachine = new PollForAvailabilityStateMachine()
        {
            Driver = this
        };
        _pollForAvailabilityStateMachine.Start();
    }

    // called when there's an error communicating with the music server; <ex> may be null, otherwise
    // it will be information about the error
    void OnMusicServerError(Exception ex)
    {
        if (MusicServerIsAvailable)
        {
            ReportAlert(MessageAlertType.Warning, "Music server \"{0}\" error: {1}",
				_musicServerSpec,
                ((ex != null) ? Util.FormatExceptionMessage(ex) : "(no specific error information)"));
        }
        DisconnectFromMusicServer();
    }

    void StopPollingForAvailability()
    {
        if (_checkAvailabilityWorkItem != null)
        {
            _checkAvailabilityWorkItem.Abort();
            _checkAvailabilityWorkItem = null;
        }

        _pollForAvailabilityStateMachine.IsOrphaned = true;
        _pollForAvailabilityStateMachine = null;
    }

    void DisconnectFromMusicServer()
    {
        if (_musicServer != null)
        {
#if !SILVERLIGHT
            var closeChannelStateMachine = new CloseChannelStateMachine()
            {
                Driver = this,
                ClientChannel = (IClientChannel)_musicServer
            };
            _closeChannelStateMachines.Add(closeChannelStateMachine);
            closeChannelStateMachine.Start();
#endif
            _musicServer = null;
        }
        _status = null;
    }

#if !SILVERLIGHT
    // list of currently active state machines that are used to close active channel(s) to
    // the music server
    List<CloseChannelStateMachine> _closeChannelStateMachines = new List<CloseChannelStateMachine>();

    class CloseChannelStateMachine : StateMachine
    {
        public MusicPlayerDriver Driver;
        public IClientChannel ClientChannel;

        protected override IEnumerable<bool> Implementation()
        {
			if (Driver._musicServer == null)
				yield break;
            var channel = (IClientChannel)Driver._musicServer;
            AsyncResultHolder.Run(this, Driver.WorkQueue,
                (onDone) => channel.BeginClose(onDone, null),
                channel.EndClose, null);
            yield return true; // wait for async reply
            Driver._closeChannelStateMachines.Remove(this);
        }
    }
#endif

    class PollForAvailabilityStateMachine : StateMachine
    {
        public MusicPlayerDriver Driver;

        protected override IEnumerable<bool> Implementation()
        {
            while (Driver.IsEnabled)
            {
                if (Driver._musicServer == null)
                    Driver._musicServer = Driver.CreateChannelToMusicServer();

				bool availabilityChanged = false;
                if (Driver.MusicServerIsAvailable)
				{
					// last time we checked, the music server was available -- see if that's still
					// true by sending a "Hello" request
					var hello = AsyncResultHolder.Run(this, Driver.WorkQueue,
						(onDone) => Driver._musicServer.BeginHello(onDone, null),
						Driver._musicServer.EndHello, Driver.OnMusicServerError);
					yield return true; // wait for async reply
					if (!hello.Success)
					{
						// the music server is no longer available
                        Driver._status = null;
                        Driver.ReportAlert(MessageAlertType.Warning,
                            "Music server \"{0}\" is not available", Driver._musicServerSpec);
						//Driver.Trace(true, "Music server is not available");
						availabilityChanged = true;
					}
				}
				else
                {
					// last time we checked, the music server was not available -- see if it's now
					// available by sending a "GetDetailedStatus" request (we use that request
					// because we want up-to-date status anyway)
					var getDetailedStatus = AsyncResultHolder<MusicServiceDetailedStatus>.Run(this,
						Driver.WorkQueue,
						(onDone) => Driver._musicServer.BeginGetDetailedStatus(onDone, null),
						Driver._musicServer.EndGetDetailedStatus, Driver.OnMusicServerError);
					yield return true; // wait for async reply

					if (getDetailedStatus.Result != null)
					{
						// the music server is now available...

                        // store the retrieved status in <Driver._status>
                        Driver._status = getDetailedStatus.Result;

                        // assume that music server state may have changed
                        Driver._libraryChangeCount++;
                        Driver._queueChangeCount++;

                        // report information alert
                        Driver.ReportAlert(MessageAlertType.Information,
                            "Music server \"{0}\" is available", Driver._musicServerSpec);
						//Driver.Trace(true, "Music server is available");

                        // music server availability has changed
						availabilityChanged = true;
					}
                }

				if (availabilityChanged)
                {
#if false
                    // inform clients of the change to the availability of the music server, and
                    // also give them the music server settings and queue state
                    var update = new SyncNodeUpdate()
                    {
                        Path = SyncPath.FromString(Driver.Name),
                        Value = Driver.MusicServerIsAvailable ? (object)true : null
                    };
                    Driver.BroadcastMessageElements(
                        delegate(Guid sessionId, MessageBuilder messageBuilder)
                        {
                            update.Serialize(messageBuilder.Writer, false);
                            Driver.SerializeSettingsSummary(messageBuilder.Writer);
                        });
                    Driver.BroadcastQueueSaveState();
#else
                    Driver.BroadcastMessageElements(
                        delegate(Guid sessionId, MessageBuilder messageBuilder)
                        {
                            Driver.SerializeStateForClient(sessionId, messageBuilder.Writer);
                        });
#endif
                }

                Driver._checkAvailabilityWorkItem = Driver.WorkQueue.Add(
                    Driver.MusicServerIsAvailable ? 10000 : 1000, NextState);
                yield return true; // wait for timeout
                Driver._checkAvailabilityWorkItem = null;
            }
        }
    }

    void PrepareForNextPollForAvailability()
    {
        _checkAvailabilityWorkItem = WorkQueue.Add(
            MusicServerIsAvailable ? 60000 : 1000, PollForAvailability);
    }

    void PollForAvailability()
    {
        PrepareForNextPollForAvailability();
    }

    public override void SerializeStateForClient(Guid sessionId, XmlWriter writer)
    {
        SerializeSyncNodeUpdate(writer, (MusicServerIsAvailable ? (object)true : null), Name);
        SerializeSyncNodeUpdate(writer, _libraryChangeCount, Name, "LibraryChangeCount");
        SerializeSyncNodeUpdate(writer, _queueChangeCount, Name, "QueueChangeCount");
        SerializeSyncNodeUpdate(writer, QueueSaveStateString, Name, "QueueSaveState");
        SerializeMusicServiceStatus(writer);
        SerializeQueueSummary(writer);
		SerializeSettingsSummary(writer);
    }

    void SerializeMusicServiceStatus(XmlWriter writer)
    {
        SerializeSyncNodeUpdate(writer,
			((_status != null) ? _status.PlayState.ToString() : null),
			Name, "PlayState");
        SerializeSyncNodeUpdate(writer,
			((_status != null) ? (object)_status.AlbumId : null),
			Name, "AlbumId");
        SerializeSyncNodeUpdate(writer,
            ((_status != null) ? (object)_status.TrackNumber : null),
			Name, "TrackNumber");
        SerializeSyncNodeUpdate(writer,
            ((_status != null) ? (object)_status.Position : null),
			Name, "Position");
        SerializeSyncNodeUpdate(writer,
            ((_status != null) ? (object)_status.QueueIndex : null),
			Name, "QueueIndex");
    }

    void SerializeQueueSummary(XmlWriter writer)
    {
        string summaryAsString = String.Format("{0};{1}",
            ((_status != null) ? _status.QueuedTrackCount : 0),
            ((_status != null) ? (int)_status.QueuedTrackDuration : 0));
        SerializeSyncNodeUpdate(writer, summaryAsString, Name, "QueueSummary");
    }

	void SerializeSettingsSummary(XmlWriter writer)
	{
        // set <settings> to the current MusicServiceSettings, or to blank settings if we haven't
        // received MusicServiceSettings from the music server
        MusicServiceSettings settings;
        if ((_status == null) || ((settings = _status.Settings) == null))
            settings = new MusicServiceSettings(); // blank settings

        // serialize <settings>
        SerializeSyncNodeUpdate(writer,
            settings.IsLoopMode.HasValue ? (object)settings.IsLoopMode.Value : null,
            Name, "IsLoopMode");
        SerializeSyncNodeUpdate(writer,
            settings.IsShuffleMode.HasValue ? (object)settings.IsShuffleMode.Value : null,
            Name, "IsShuffleMode");
        SerializeSyncNodeUpdate(writer,
            settings.PlaybackRate.HasValue ? (object)settings.PlaybackRate.Value : null,
            Name, "PlaybackRate");
	}

	void BroadcastMusicServiceStatus()
	{
        BroadcastMessageElements(delegate(Guid sessionId, MessageBuilder messageBuilder)
        {
			SerializeMusicServiceStatus(messageBuilder.Writer);
        });
	}

    public override void ProcessSyncNodeUpdate(Guid originator, SyncNodeUpdate update,
        MessageBuilder replyBuilder)
    {
        update.Path.VerifyPathLength(2);
        switch (update.Path.MatchRequiredPart(1, "IsLoopMode", "IsShuffleMode", "PlaybackRate"))
        {

        // the order of these cases is the same as the order of strings in MatchRequiredPart above

        case 0: // "IsLoopMode"

           QueueRequest(new SetSettingsRequest(this, originator, update)
           {
                Settings = new MusicServiceSettings()
                {
                    IsLoopMode = update.GetTypedValue<bool>()
                }
            }, true);
            break;

        case 1: // "IsShuffleMode"

            QueueRequest(new SetSettingsRequest(this, originator, update)
            {
                Settings = new MusicServiceSettings()
                {
                    IsShuffleMode = update.GetTypedValue<bool>()
                }
            }, true);
            break;
        
        case 2: // "PlaybackRate"

            QueueRequest(new SetSettingsRequest(this, originator, update)
            {
                Settings = new MusicServiceSettings()
                {
                    PlaybackRate = update.GetTypedValue<double>()
                }
            }, true);
            break;

        }
    }

    public override void ProcessSyncNodeCommand(Guid originator, SyncNodeCommand command, MessageBuilder replyBuilder)
    {
		var parsedCommand = new ParsedSyncNodeCommand(command.Verb);

        // throw SyncException if <_musicServer> is null
        CheckMusicServerAvailability();

		switch (parsedCommand.Name)
		{

        case "GetDetailedStatus":

            QueueRequest(new GetDetailedStatusRequest(this, originator, command,
				parsedCommand.GetRequiredGuidArgument("RecipientId")), false);
			break;

        case "GetArtists":

            QueueRequest(new GetArtistsRequest(this, originator, command,
				parsedCommand.GetRequiredGuidArgument("RecipientId")), false);
			break;

        case "GetAlbumsOfArtist":

            QueueRequest(new GetAlbumsOfArtistRequest(this, originator, command,
				parsedCommand.GetRequiredGuidArgument("RecipientId"))
            {
                ArtistName = parsedCommand.GetRequiredArgument("ArtistName")
            }, false);
			break;

        case "GetAlbumDetails":

            QueueRequest(new GetAlbumDetailsRequest(this, originator, command,
				parsedCommand.GetRequiredGuidArgument("RecipientId"))
            {
                AlbumId = parsedCommand.GetRequiredArgument("AlbumId")
            }, false);
			break;

        case "Search":

            var sr = new SearchRequest();
            sr.Find = parsedCommand.GetRequiredArgument("Find");
            sr.MaxArtists = parsedCommand.GetRequiredIntArgument("MaxArtists");
            sr.MaxAlbums = parsedCommand.GetRequiredIntArgument("MaxAlbums");
            sr.MaxTracks = parsedCommand.GetRequiredIntArgument("MaxTracks");
            sr.GetAlbumArt = parsedCommand.GetOptionalBooleanArgument("GetAlbumArt", false);
            QueueRequest(new MusicSearchRequest(this, originator, command,
                parsedCommand.GetRequiredGuidArgument("RecipientId"))
            {
                Request = sr
            }, false);
            break;

        case "GetQueueDetails":

            QueueRequest(new GetQueueDetailsRequest(this, originator, command,
				parsedCommand.GetRequiredGuidArgument("RecipientId")), false);
            break;

        case "GetPlaylists":

            QueueRequest(new GetPlaylistsRequest(this, originator, command,
                parsedCommand.GetRequiredGuidArgument("RecipientId")), false);
            break;

        case "LoadPlaylist":

            QueueRequest(new LoadPlaylistRequest(this, originator, command)
            {
                PlaylistName = parsedCommand.GetRequiredArgument("PlaylistName")
            }, false);
            break;

        case "SaveQueue":

            QueueRequest(new SaveQueueRequest(this, originator, command)
            {
                PlaylistName = parsedCommand.GetRequiredArgument("PlaylistName")
            }, false);
            break;

        case "DeletePlaylist":

            QueueRequest(new DeletePlaylistRequest(this, originator, command)
            {
                PlaylistName = parsedCommand.GetRequiredArgument("PlaylistName")
            }, false);
            break;

        case "ForgetPlaylist":

            QueueRequest(new ForgetPlaylistRequest(this, originator, command), false);
            break;

        case "ClearQueue":

            IsQueueDirty = true;
            QueueRequest(new GenericRequest(this, originator, command, "ClearQueue",
                (onDone) => _musicServer.BeginClearQueue(onDone, null),
                _musicServer.EndClearQueue), false);
            break;

        case "Play":

            QueueRequest(new GenericRequest(this, originator, command, "Play",
                (onDone) => _musicServer.BeginPlay(onDone, null), _musicServer.EndPlay), false);
            break;

        case "Pause":

            QueueRequest(new GenericRequest(this, originator, command, "Pause",
                (onDone) => _musicServer.BeginPause(onDone, null), _musicServer.EndPause), false);
            break;

        case "Stop":

            QueueRequest(new GenericRequest(this, originator, command, "Stop",
                (onDone) => _musicServer.BeginStop(onDone, null), _musicServer.EndStop), false);
            break;

        case "FastForward":

            QueueRequest(new GenericRequest(this, originator, command, "FastForward",
                (onDone) => _musicServer.BeginFastForward(onDone, null),
                _musicServer.EndFastForward), false);
            break;

        case "FastReverse":

            QueueRequest(new GenericRequest(this, originator, command, "FastReverse",
                (onDone) => _musicServer.BeginFastReverse(onDone, null),
                _musicServer.EndFastReverse), false);
            break;

        case "NextTrack":

            QueueRequest(new GenericRequest(this, originator, command, "NextTrack",
                (onDone) => _musicServer.BeginNextTrack(onDone, null),
                _musicServer.EndNextTrack), false);
            break;

        case "PreviousTrack":

            QueueRequest(new GenericRequest(this, originator, command, "PreviousTrack",
                (onDone) => _musicServer.BeginPreviousTrack(onDone, null),
                _musicServer.EndPreviousTrack), false);
            break;

        case "AddToQueue":

            IsQueueDirty = true;
            QueueRequest(new AddToQueueRequest(this, originator, command,
                parsedCommand.GetRequiredGuidArgument("RecipientId"))
            {
                AlbumId = parsedCommand.GetRequiredArgument("AlbumId"),
                TrackNumbers = parsedCommand.GetRequiredIntListArgument("TrackNumbers"),
            }, false);
            break;

        case "RemoveFromQueue":

            IsQueueDirty = true;
            QueueRequest(new RemoveFromQueueRequest(this, originator, command,
                parsedCommand.GetRequiredGuidArgument("RecipientId"))
            {
                AlbumId = parsedCommand.GetRequiredArgument("AlbumId"),
                TrackNumbers = parsedCommand.GetRequiredIntListArgument("TrackNumbers"),
            }, false);
            break;

        case "RemoveFromQueueMultipleAlbums":

            IsQueueDirty = true;
            QueueRequest(new RemoveFromQueueMultipleAlbumsRequest(this, originator, command)
            {
                TrackIds = GetRequiredTrackIdListArgument(parsedCommand, "TrackIds")
            }, false);
            break;

        case "ChangeQueue":

            IsQueueDirty = true;
            QueueRequest(new ChangeQueueRequest(this, originator, command)
            {
                TrackIds = GetRequiredTrackIdListArgument(parsedCommand, "TrackIds")
            }, false);
            break;

        case "PlayAlbum":

            // clear the queue and play the specified album, starting at the specified track;
			// all tracks of the album are in the queue so that "|<" and ">|" navigate among all
			// tracks
            QueueRequest(new PlayAlbumRequest(this, originator, command,
                parsedCommand.GetRequiredGuidArgument("RecipientId"))
            {
                AlbumId = parsedCommand.GetRequiredArgument("AlbumId"),
                StartAtTrackNumber = parsedCommand.GetRequiredIntArgument("StartAtTrackNumber"),
            }, false);
            break;

        case "PlayQueueItem":

            QueueRequest(new PlayQueueItemRequest(this, originator, command)
            {
                TrackId = GetRequiredTrackIdArgument(parsedCommand, "TrackId")
            }, false);
            break;

        default:

            throw new SyncException("Unknown command: {0}", parsedCommand.Name);

		}
    }

    TrackId GetRequiredTrackIdArgument(ParsedSyncNodeCommand parsedCommand, string argumentName)
    {
        string stringValue = parsedCommand.GetRequiredArgument(argumentName);
        try
        {
            return ParseTrackId(stringValue);
        }
        catch (SyncException)
        {
            throw new SyncException(
                "Expected argument \"{0}\" to be an \"album-id:track-number\", but got \"{1}\", in command: {2}",
                argumentName, stringValue, parsedCommand.StringForm);
        }
    }

    TrackId ParseTrackId(string stringValue)
    {
		try
		{
			int index = stringValue.LastIndexOf(':');
			if (index < 0)
				throw new FormatException();
			string albumId = stringValue.Substring(0, index).Trim();
			if (albumId.Length == 0)
				throw new FormatException();
			int trackNumber = XmlConvert.ToInt32(stringValue.Substring(index + 1));
			return new TrackId()
			{
				AlbumId = albumId,
				TrackNumber = trackNumber
			};
		}
		catch (Exception ex)
		{
            if ((ex is FormatException) || (ex is OverflowException))
            {
                throw new SyncException(
                    "Expected \"album-id:track-number\", but got: {0}", stringValue);
            }
            else
                throw;
		}
    }

    TrackId[] GetRequiredTrackIdListArgument(ParsedSyncNodeCommand parsedCommand, string argumentName)
    {
        string[] trackIdStringList = parsedCommand.GetRequiredStringListArgument(argumentName);
        TrackId[] trackIds = new TrackId[trackIdStringList.Length];
        for (int index = 0; index < trackIds.Length; index++)
        {
            string stringValue = trackIdStringList[index];
            try
            {
                trackIds[index] = ParseTrackId(stringValue);
            }
            catch (SyncException)
            {
                throw new SyncException(
                    "Expected argument \"{0}\" to be a \"|\"-separated list of \"album-id:track-number\", but one of them was \"{1}\", in command: {2}",
                    argumentName, stringValue, parsedCommand.StringForm);
            }
        }

        return trackIds;
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// IDisposable Overrides
	//

    public override void Dispose()
    {
		// TODO
        base.Dispose();
    }

	///////////////////////////////////////////////////////////////////////////////////////////////
	// Helper Methods
	//

	protected void CheckMusicServerAvailability()
	{
		if (_musicServer == null)
		{
			throw new SyncException("Music server \"{0}\" is not available",
				_musicServerSpec);
		}
	}

    protected override void MergeRequests(IList<Driver.Request> futureRequests)
    {
		// TODO
    }

    void SetQueueSaveState(QueueSaveState queueSaveState, string associatedPlaylistName)
    {
        _queueSaveState = queueSaveState;
        _queueAssociatedPlaylistName = associatedPlaylistName;
        BroadcastQueueSaveState();
    }

    void BroadcastQueueSaveState()
    {
        BroadcastSyncNodeUpdate(SyncPath.Build(Name, "QueueSaveState"), QueueSaveStateString,
            NoOriginators);
    }

    string QueueSaveStateString
    {
        get
        {
            if (_queueSaveState == QueueSaveState.New)
            {
                if ((_status != null) && (_status.QueuedTrackCount > 0))
                    return "NewNotEmpty:";
                else
                    return "NewEmpty:";
            }
            else
                return String.Format("{0}:{1}", _queueSaveState, _queueAssociatedPlaylistName);
        }
    }

    bool IsQueueDirty
    {
        get
        {
            return ((_queueSaveState == QueueSaveState.LoadedModified) ||
                    (_queueSaveState == QueueSaveState.SavedModified) ||
                    ((_queueSaveState == QueueSaveState.New) &&
                     (_status != null) && (_status.QueuedTrackCount > 0)));
        }
        set
        {
            if (value)
            {
                if (_queueSaveState == QueueSaveState.Loaded)
                    SetQueueSaveState(QueueSaveState.LoadedModified, _queueAssociatedPlaylistName);
                else
                if (_queueSaveState == QueueSaveState.Saved)
                    SetQueueSaveState(QueueSaveState.SavedModified, _queueAssociatedPlaylistName);
            }
            else
            {
                if (_queueSaveState == QueueSaveState.LoadedModified)
                    SetQueueSaveState(QueueSaveState.Loaded, _queueAssociatedPlaylistName);
                else
                if (_queueSaveState == QueueSaveState.SavedModified)
                    SetQueueSaveState(QueueSaveState.Saved, _queueAssociatedPlaylistName);
            }
        }
    }

    ///////////////////////////////////////////////////////////////////////////////////////////////
    // MusicPlayerRequest and Derived Types
    //

    abstract class MusicPlayerRequest : Driver.Request
    {
        protected new MusicPlayerDriver Driver
        {
            [DebuggerStepThrough]
            get
            {
                return (MusicPlayerDriver)base.Driver;
            }
        }

        public Guid? RecipientId;

        public MusicPlayerRequest(MusicPlayerDriver driver, Guid originator,
            SyncNodeOperation operation)
        {
            base.Driver = driver;
            Originators = new List<Guid>() { originator };
            Operation = operation;
        }

        public MusicPlayerRequest(MusicPlayerDriver driver, Guid originator,
            SyncNodeOperation operation, Guid recipientId)
        {
            base.Driver = driver;
            Originators = new List<Guid>() { originator };
            Operation = operation;
            RecipientId = recipientId;
        }

        protected void SerializeRecipientId(XmlWriter writer)
        {
            if (RecipientId.HasValue)
                writer.WriteAttributeString("Recipient", XmlConvert.ToString(RecipientId.Value));
        }
    }

    class SetSettingsRequest : MusicPlayerRequest
    {
		public MusicServiceSettings Settings;

        public SetSettingsRequest(MusicPlayerDriver driver, Guid originator,
                SyncNodeOperation operation) :
            base(driver, originator, operation)
        {
        }

        protected override IEnumerable<bool> Implementation()
        {
            Driver.Trace(true, "HW: Updated settings");
            var setSettings = AsyncResultHolder.Run(this,
                Driver.WorkQueue,
                (onDone) => Driver._musicServer.BeginSetSettings(Settings, onDone, null),
                Driver._musicServer.EndSetSettings, Driver.OnMusicServerError);
            yield return true; // wait for async reply
            if (!setSettings.Success)
            {
                Error = "SetSettings failed";
                yield break;
            }
        }
	}

    class GetDetailedStatusRequest : MusicPlayerRequest
    {
        public GetDetailedStatusRequest(MusicPlayerDriver driver, Guid originator,
                SyncNodeOperation operation, Guid recipientId) :
            base(driver, originator, operation, recipientId)
        {
        }

        protected override IEnumerable<bool> Implementation()
        {
            var getDetailedStatus = AsyncResultHolder<MusicServiceDetailedStatus>.Run(this,
                Driver.WorkQueue,
                (onDone) => Driver._musicServer.BeginGetDetailedStatus(onDone, null),
                Driver._musicServer.EndGetDetailedStatus, Driver.OnMusicServerError);
            yield return true; // wait for async reply
            if (getDetailedStatus.Result == null)
            {
                Error = "GetDetailedStatus failed";
                yield break;
            }

            MusicServiceDetailedStatus status = getDetailedStatus.Result;

            Driver.SendMessageElements(SingleOriginator,
                delegate(MessageBuilder messageBuilder)
            {
				XmlWriter writer = messageBuilder.Writer;
                writer.WriteStartElement("Parcel");
                SerializeRecipientId(writer);
                writer.WriteStartElement("DetailedStatus");

                writer.WriteAttributeString("PlayState", status.PlayState.ToString());
                writer.WriteAttributeString("AlbumId", status.AlbumId);
                writer.WriteAttributeString("TrackNumber",
                    XmlConvert.ToString(status.TrackNumber));
                writer.WriteAttributeString("Position", XmlConvert.ToString(status.Position));
                writer.WriteAttributeString("QueueIndex", XmlConvert.ToString(status.QueueIndex));

                writer.WriteAttributeString("ArtistName", status.ArtistName);
                writer.WriteAttributeString("AlbumName", status.AlbumName);

                writer.WriteAttributeString("TrackName", status.TrackName);
                writer.WriteAttributeString("TrackDuration",
                    XmlConvert.ToString(status.TrackDuration));
                writer.WriteAttributeString("TrackYear",
                    XmlConvert.ToString(status.TrackYear));
                writer.WriteAttributeString("TrackComposer", status.TrackComposer);

                writer.WriteAttributeString("QueuedTrackCount",
                    XmlConvert.ToString(status.QueuedTrackCount));
                writer.WriteAttributeString("QueuedTrackDuration",
                    XmlConvert.ToString(status.QueuedTrackDuration));

                writer.WriteAttributeString("Volume",
                    XmlConvert.ToString(status.Settings.Volume.Value));
                writer.WriteAttributeString("IsMuted",
                    XmlConvert.ToString(status.Settings.IsMuted.Value));
                writer.WriteAttributeString("PlaybackRate",
                    XmlConvert.ToString(status.Settings.PlaybackRate.Value));
                writer.WriteAttributeString("IsLoopMode",
                    XmlConvert.ToString(status.Settings.IsLoopMode.Value));
                writer.WriteAttributeString("IsShuffleMode",
                    XmlConvert.ToString(status.Settings.IsShuffleMode.Value));

                if (status.AlbumArt != null)
                {
                    writer.WriteStartElement("AlbumArt");
                    writer.WriteBase64(status.AlbumArt, 0, status.AlbumArt.Length);
                    writer.WriteEndElement(); // end "AlbumArt"
                }

                writer.WriteEndElement(); // end "DetailedStatus"
                writer.WriteEndElement(); // end "Parcel"
            });
        }
	}

    class GetArtistsRequest : MusicPlayerRequest
    {
        public GetArtistsRequest(MusicPlayerDriver driver, Guid originator,
                SyncNodeOperation operation, Guid recipientId) :
            base(driver, originator, operation, recipientId)
        {
        }

        protected override IEnumerable<bool> Implementation()
        {
            var getArtists = AsyncResultHolder<ArtistInfo[]>.Run(this, Driver.WorkQueue,
                (onDone) => Driver._musicServer.BeginGetArtists(onDone, null),
                Driver._musicServer.EndGetArtists, Driver.OnMusicServerError);
            yield return true; // wait for async reply
            if (getArtists.Result == null)
            {
                Error = "GetArtists failed";
                yield break;
            }

            Driver.SendMessageElements(SingleOriginator,
                delegate(MessageBuilder messageBuilder)
            {
                messageBuilder.Writer.WriteStartElement("Parcel");
                SerializeRecipientId(messageBuilder.Writer);
                messageBuilder.Writer.WriteStartElement("Artists");

                foreach (ArtistInfo artist in getArtists.Result)
                {
                    messageBuilder.Writer.WriteStartElement("Artist");
                    messageBuilder.Writer.WriteAttributeString("Name", artist.Name);
                    messageBuilder.Writer.WriteAttributeString("SortKey", artist.SortKey);
                    messageBuilder.Writer.WriteEndElement();
                }

                messageBuilder.Writer.WriteEndElement(); // end "Artists"
                messageBuilder.Writer.WriteEndElement(); // end "Parcel"
            });
        }
	}

    class GetAlbumsOfArtistRequest : MusicPlayerRequest
    {
        public string ArtistName;

        public GetAlbumsOfArtistRequest(MusicPlayerDriver driver, Guid originator,
                SyncNodeOperation operation, Guid recipientId) :
            base(driver, originator, operation, recipientId)
        {
        }

        protected override IEnumerable<bool> Implementation()
        {
            var getAlbumsOfArtist = AsyncResultHolder<AlbumInfo[]>.Run(this, Driver.WorkQueue,
                (onDone) => Driver._musicServer.BeginGetAlbumsOfArtist(ArtistName,
					GetAlbumsOfArtistOptions.GetAlbumArt, onDone, null),
                Driver._musicServer.EndGetAlbumsOfArtist, Driver.OnMusicServerError);
            yield return true; // wait for async reply
            if (getAlbumsOfArtist.Result == null)
            {
                Error = "GetAlbumsOfArtist failed";
                yield break;
            }

            Driver.SendMessageElements(SingleOriginator,
                delegate(MessageBuilder messageBuilder)
            {
                messageBuilder.Writer.WriteStartElement("Parcel");
                SerializeRecipientId(messageBuilder.Writer);
                messageBuilder.Writer.WriteStartElement("Albums");
                messageBuilder.Writer.WriteAttributeString("ArtistName", ArtistName);

                foreach (AlbumInfo album in getAlbumsOfArtist.Result)
                {
                    messageBuilder.Writer.WriteStartElement("Album");
                    messageBuilder.Writer.WriteAttributeString("Id", album.Id);
                    messageBuilder.Writer.WriteAttributeString("Name", album.Name);
                    if (album.AlbumArt != null)
                    {
                        messageBuilder.Writer.WriteStartElement("AlbumArt");
                        messageBuilder.Writer.WriteBase64(album.AlbumArt, 0, album.AlbumArt.Length);
                        messageBuilder.Writer.WriteEndElement(); // end "AlbumArt"
                    }
                    messageBuilder.Writer.WriteEndElement(); // end "Album"
                }

                messageBuilder.Writer.WriteEndElement(); // end "Albums"
                messageBuilder.Writer.WriteEndElement(); // end "Parcel"
            });
        }
	}

    class GetAlbumDetailsRequest : MusicPlayerRequest
    {
        public string AlbumId;

        public GetAlbumDetailsRequest(MusicPlayerDriver driver, Guid originator,
                SyncNodeOperation operation, Guid recipientId) :
            base(driver, originator, operation, recipientId)
        {
        }

        protected override IEnumerable<bool> Implementation()
        {
            var getAlbumDetails = AsyncResultHolder<AlbumDetails>.Run(this, Driver.WorkQueue,
                (onDone) => Driver._musicServer.BeginGetAlbumDetails(AlbumId, onDone, null),
                Driver._musicServer.EndGetAlbumDetails, Driver.OnMusicServerError);
            yield return true; // wait for async reply
            if (getAlbumDetails.Result == null)
            {
                Error = "GetAlbumDetails failed";
                yield break;
            }

            Driver.SendMessageElements(SingleOriginator, delegate(MessageBuilder messageBuilder)
            {
                messageBuilder.Writer.WriteStartElement("Parcel");
                SerializeRecipientId(messageBuilder.Writer);
                messageBuilder.Writer.WriteStartElement("AlbumDetails");

                AlbumDetails details = getAlbumDetails.Result;

                messageBuilder.Writer.WriteAttributeString("Id", AlbumId);
                messageBuilder.Writer.WriteAttributeString("ArtistName", details.ArtistName);
                messageBuilder.Writer.WriteAttributeString("Name", details.Name);

                if (details.AlbumArt != null)
                {
                    messageBuilder.Writer.WriteStartElement("AlbumArt");
                    messageBuilder.Writer.WriteBase64(details.AlbumArt, 0, details.AlbumArt.Length);
                    messageBuilder.Writer.WriteEndElement(); // end "AlbumArt"
                }

                foreach (TrackDetails track in details.Tracks)
                {
                    messageBuilder.Writer.WriteStartElement("Track");
                    messageBuilder.Writer.WriteAttributeString("TrackNumber",
                        XmlConvert.ToString(track.TrackNumber));
                    messageBuilder.Writer.WriteAttributeString("Name", track.Name);
                    messageBuilder.Writer.WriteAttributeString("Duration",
                        XmlConvert.ToString(track.Duration));
                    messageBuilder.Writer.WriteAttributeString("Year",
                        XmlConvert.ToString(track.Year));
                    messageBuilder.Writer.WriteAttributeString("Composer", track.Composer);
                    messageBuilder.Writer.WriteEndElement(); // end "Track"
                }

                messageBuilder.Writer.WriteEndElement(); // end "AlbumDetails"
                messageBuilder.Writer.WriteEndElement(); // end "Parcel"
            });
        }
	}

    class MusicSearchRequest : MusicPlayerRequest
    {
        public SearchRequest Request;

        public MusicSearchRequest(MusicPlayerDriver driver, Guid originator,
                SyncNodeOperation operation, Guid recipientId) :
            base(driver, originator, operation, recipientId)
        {
        }

        protected override IEnumerable<bool> Implementation()
        {
			SearchResult result;
			if (String.IsNullOrEmpty(Request.Find))
			{
				var hello = AsyncResultHolder.Run(this, Driver.WorkQueue,
					(onDone) => Driver._musicServer.BeginHello(onDone, null),
					Driver._musicServer.EndHello, Driver.OnMusicServerError);
				yield return true; // wait for async reply
				if (!hello.Success)
				{
					Error = "Hello failed";
					yield break;
				}
				result = new SearchResult();
			}
			else
			{
				var search = AsyncResultHolder<SearchResult>.Run(this, Driver.WorkQueue,
					(onDone) => Driver._musicServer.BeginSearch(Request, onDone, null),
					Driver._musicServer.EndSearch, Driver.OnMusicServerError);
				yield return true; // wait for async reply
				if (search.Result == null)
				{
					Error = "Search failed";
					yield break;
				}
				result = search.Result;
			}

            Driver.SendMessageElements(SingleOriginator, delegate(MessageBuilder messageBuilder)
            {
                messageBuilder.Writer.WriteStartElement("Parcel");
                SerializeRecipientId(messageBuilder.Writer);
                messageBuilder.Writer.WriteStartElement("DoneSearch");

                messageBuilder.Writer.WriteAttributeString("Find", Request.Find);
                messageBuilder.Writer.WriteAttributeString("FoundArtistCount",
					XmlConvert.ToString(result.FoundArtistCount));
                messageBuilder.Writer.WriteAttributeString("FoundAlbumCount",
					XmlConvert.ToString(result.FoundAlbumCount));
                messageBuilder.Writer.WriteAttributeString("FoundTrackCount",
					XmlConvert.ToString(result.FoundTrackCount));

				if (result.FoundArtists != null)
				{
					foreach (FoundArtist foundArtist in result.FoundArtists)
					{
						messageBuilder.Writer.WriteStartElement("FoundArtist");
						messageBuilder.Writer.WriteAttributeString("ArtistName",
							foundArtist.ArtistName);
						messageBuilder.Writer.WriteEndElement(); // end "FoundArtist"
					}
				}

				if (result.FoundAlbums != null)
				{
					foreach (FoundAlbum foundAlbum in result.FoundAlbums)
					{
						messageBuilder.Writer.WriteStartElement("FoundAlbum");
						messageBuilder.Writer.WriteAttributeString("AlbumId",
							foundAlbum.AlbumId);
						messageBuilder.Writer.WriteAttributeString("ArtistName",
							foundAlbum.ArtistName);
						messageBuilder.Writer.WriteAttributeString("AlbumName",
							foundAlbum.AlbumName);

						if (foundAlbum.AlbumArt != null)
						{
							messageBuilder.Writer.WriteStartElement("FoundAlbumArt");
							messageBuilder.Writer.WriteBase64(foundAlbum.AlbumArt, 0,
								foundAlbum.AlbumArt.Length);
							messageBuilder.Writer.WriteEndElement(); // end "FoundAlbumArt"
						}

						messageBuilder.Writer.WriteEndElement(); // end "FoundAlbum"
					}
				}

				if (result.FoundTracks != null)
				{
					foreach (FoundTrack foundTrack in result.FoundTracks)
					{
						messageBuilder.Writer.WriteStartElement("FoundTrack");
						messageBuilder.Writer.WriteAttributeString("AlbumId",
							foundTrack.TrackId.AlbumId);
						messageBuilder.Writer.WriteAttributeString("TrackNumber",
							XmlConvert.ToString(foundTrack.TrackId.TrackNumber));
						messageBuilder.Writer.WriteAttributeString("ArtistName",
							foundTrack.ArtistName);
						messageBuilder.Writer.WriteAttributeString("AlbumName",
							foundTrack.AlbumName);
						messageBuilder.Writer.WriteAttributeString("TrackName",
							foundTrack.TrackName);
						messageBuilder.Writer.WriteEndElement(); // end "FoundTrack"
					}
				}

                messageBuilder.Writer.WriteEndElement(); // end "DoneSearch"
                messageBuilder.Writer.WriteEndElement(); // end "Parcel"
            });
        }
	}

    class GetQueueDetailsRequest : MusicPlayerRequest
    {
        public GetQueueDetailsRequest(MusicPlayerDriver driver, Guid originator,
                SyncNodeOperation operation, Guid recipientId) :
            base(driver, originator, operation, recipientId)
        {
        }

        protected override IEnumerable<bool> Implementation()
        {
            var getQueueDetails = AsyncResultHolder<QueueDetails>.Run(this, Driver.WorkQueue,
                (onDone) => Driver._musicServer.BeginGetQueueDetails(onDone, null),
                Driver._musicServer.EndGetQueueDetails, Driver.OnMusicServerError);
            yield return true; // wait for async reply
            if (getQueueDetails.Result == null)
            {
                Error = "GetQueueDetails failed";
                yield break;
            }

            Driver.SendMessageElements(SingleOriginator, delegate(MessageBuilder messageBuilder)
            {
                messageBuilder.Writer.WriteStartElement("Parcel");
                SerializeRecipientId(messageBuilder.Writer);
                messageBuilder.Writer.WriteStartElement("QueueDetails");

                QueueDetails details = getQueueDetails.Result;

                foreach (AlbumTrackDetails track in details.Tracks)
                {
                    messageBuilder.Writer.WriteStartElement("TrackInQueue");
                    messageBuilder.Writer.WriteAttributeString("ArtistName", track.ArtistName);
                    messageBuilder.Writer.WriteAttributeString("AlbumId", track.AlbumId);
                    messageBuilder.Writer.WriteAttributeString("AlbumName", track.AlbumName);
                    messageBuilder.Writer.WriteAttributeString("TrackNumber",
                        XmlConvert.ToString(track.TrackDetails.TrackNumber));
                    messageBuilder.Writer.WriteAttributeString("Name", track.TrackDetails.Name);
                    messageBuilder.Writer.WriteAttributeString("Duration",
                        XmlConvert.ToString(track.TrackDetails.Duration));
                    messageBuilder.Writer.WriteAttributeString("Year",
                        XmlConvert.ToString(track.TrackDetails.Year));
                    messageBuilder.Writer.WriteAttributeString("Composer",
						track.TrackDetails.Composer);
                    messageBuilder.Writer.WriteEndElement(); // end "TrackInQueue"
                }

                messageBuilder.Writer.WriteEndElement(); // end "QueueDetails"
                messageBuilder.Writer.WriteEndElement(); // end "Parcel"
            });
        }
	}

    class GetPlaylistsRequest : MusicPlayerRequest
    {
        public GetPlaylistsRequest(MusicPlayerDriver driver, Guid originator,
                SyncNodeOperation operation, Guid recipientId) :
            base(driver, originator, operation, recipientId)
        {
        }

        protected override IEnumerable<bool> Implementation()
        {
            var getPlaylists = AsyncResultHolder<PlaylistInfo[]>.Run(this, Driver.WorkQueue,
                (onDone) => Driver._musicServer.BeginGetPlaylists(onDone, null),
                Driver._musicServer.EndGetPlaylists, Driver.OnMusicServerError);
            yield return true; // wait for async reply
            if (getPlaylists.Result == null)
            {
                Error = "GetPlaylists failed";
                yield break;
            }

            Driver.SendMessageElements(SingleOriginator, delegate(MessageBuilder messageBuilder)
            {
                messageBuilder.Writer.WriteStartElement("Parcel");
                SerializeRecipientId(messageBuilder.Writer);
                messageBuilder.Writer.WriteStartElement("Playlists");

                foreach (PlaylistInfo playlist in getPlaylists.Result)
                {
                    messageBuilder.Writer.WriteStartElement("Playlist");
                    messageBuilder.Writer.WriteAttributeString("Name", playlist.Name);
                    messageBuilder.Writer.WriteEndElement(); // end "Playlist"
                }

                messageBuilder.Writer.WriteEndElement(); // end "Playlists"
                messageBuilder.Writer.WriteEndElement(); // end "Parcel"
            });
        }
	}

    class LoadPlaylistRequest : MusicPlayerRequest
    {
        public string PlaylistName;

        public LoadPlaylistRequest(MusicPlayerDriver driver, Guid originator,
                SyncNodeOperation operation) :
            base(driver, originator, operation)
        {
        }

        protected override IEnumerable<bool> Implementation()
        {
            Driver.Trace(true, "HW: Load playlist \"{0}\"", PlaylistName);
            var loadPlaylist = AsyncResultHolder.Run(this, Driver.WorkQueue,
                (onDone) => Driver._musicServer.BeginLoadPlaylist(PlaylistName, onDone, null),
                Driver._musicServer.EndLoadPlaylist, Driver.OnMusicServerError);
            yield return true; // wait for async reply
            if (!loadPlaylist.Success)
            {
                Error = "LoadPlaylist failed";
                yield break;
            }

            Driver.SetQueueSaveState(QueueSaveState.Loaded, PlaylistName);
        }
	}

    class SaveQueueRequest : MusicPlayerRequest
    {
        public string PlaylistName;

        public SaveQueueRequest(MusicPlayerDriver driver, Guid originator,
                SyncNodeOperation operation) :
            base(driver, originator, operation)
        {
        }

        protected override IEnumerable<bool> Implementation()
        {
            Driver.Trace(true, "HW: Save to playlist \"{0}\"", PlaylistName);
            var saveQueue = AsyncResultHolder.Run(this, Driver.WorkQueue,
                (onDone) => Driver._musicServer.BeginSaveQueue(PlaylistName, onDone, null),
                Driver._musicServer.EndSaveQueue, Driver.OnMusicServerError);
            yield return true; // wait for async reply
            if (!saveQueue.Success)
            {
                Error = "SaveQueue failed";
                yield break;
            }

            Driver.SetQueueSaveState(QueueSaveState.Saved, PlaylistName);
        }
	}

    class DeletePlaylistRequest : MusicPlayerRequest
    {
        public string PlaylistName;

        public DeletePlaylistRequest(MusicPlayerDriver driver, Guid originator,
                SyncNodeOperation operation) :
            base(driver, originator, operation)
        {
        }

        protected override IEnumerable<bool> Implementation()
        {
            Driver.Trace(true, "HW: Delete playlist \"{0}\"", PlaylistName);
            var deletePlaylist = AsyncResultHolder.Run(this, Driver.WorkQueue,
                (onDone) => Driver._musicServer.BeginDeletePlaylist(PlaylistName, onDone, null),
                Driver._musicServer.EndDeletePlaylist, Driver.OnMusicServerError);
            yield return true; // wait for async reply
            if (!deletePlaylist.Success)
            {
                Error = "DeletePlaylist failed";
                yield break;
            }

            Driver.SetQueueSaveState(QueueSaveState.New, null);
        }
	}

    class ForgetPlaylistRequest : MusicPlayerRequest
    {
        public ForgetPlaylistRequest(MusicPlayerDriver driver, Guid originator,
                SyncNodeOperation operation) :
            base(driver, originator, operation)
        {
        }

        protected override IEnumerable<bool> Implementation()
        {
            Driver.SetQueueSaveState(QueueSaveState.New, null);
            yield break;
        }
	}

    class AddToQueueRequest : MusicPlayerRequest
    {
        public string AlbumId;
        public int[] TrackNumbers;

        public AddToQueueRequest(MusicPlayerDriver driver, Guid originator,
                SyncNodeOperation operation, Guid recipientId) :
            base(driver, originator, operation, recipientId)
        {
        }

        protected override IEnumerable<bool> Implementation()
        {
            var trackIds = new TrackId[TrackNumbers.Length];
            for (int index = 0; index < trackIds.Length; index++)
            {
                trackIds[index] = new TrackId()
                {
                    AlbumId = AlbumId,
                    TrackNumber = TrackNumbers[index]
                };
            }

            Driver.Trace(true, "HW: Add {0} track(s) to queue", trackIds.Length);
            var getAlbumDetails = AsyncResultHolder.Run(this, Driver.WorkQueue,
                (onDone) => Driver._musicServer.BeginAddToQueue(trackIds, onDone, null),
                Driver._musicServer.EndAddToQueue, Driver.OnMusicServerError);
            yield return true; // wait for async reply
            if (!getAlbumDetails.Success)
            {
                Error = "AddToQueue failed";
                yield break;
            }

            Driver.SendMessageElements(SingleOriginator, delegate(MessageBuilder messageBuilder)
            {
                messageBuilder.Writer.WriteStartElement("Parcel");
                SerializeRecipientId(messageBuilder.Writer);
                messageBuilder.Writer.WriteStartElement("DoneAddToQueue");

                foreach (TrackId trackId in trackIds)
                {
                    messageBuilder.Writer.WriteStartElement("AddedTrackToQueue");
                    messageBuilder.Writer.WriteAttributeString("AlbumId", trackId.AlbumId);
                    messageBuilder.Writer.WriteAttributeString("TrackNumber",
                        XmlConvert.ToString(trackId.TrackNumber));
                    messageBuilder.Writer.WriteEndElement(); // end "AddedTrackToQueue"
                }

                messageBuilder.Writer.WriteEndElement(); // end "DoneAddToQueue"
                messageBuilder.Writer.WriteEndElement(); // end "Parcel"
            });
        }
	}

    class RemoveFromQueueRequest : MusicPlayerRequest
    {
        public string AlbumId;
        public int[] TrackNumbers;

        public RemoveFromQueueRequest(MusicPlayerDriver driver, Guid originator,
                SyncNodeOperation operation, Guid recipientId) :
            base(driver, originator, operation, recipientId)
        {
        }

        protected override IEnumerable<bool> Implementation()
        {
            var trackIds = new TrackId[TrackNumbers.Length];
            for (int index = 0; index < trackIds.Length; index++)
            {
                trackIds[index] = new TrackId()
                {
                    AlbumId = AlbumId,
                    TrackNumber = TrackNumbers[index]
                };
            }

            Driver.Trace(true, "HW: Remove {0} track(s) from queue", trackIds.Length);
            var removeFromQueue = AsyncResultHolder.Run(this, Driver.WorkQueue,
                (onDone) => Driver._musicServer.BeginRemoveFromQueue(trackIds, onDone, null),
                Driver._musicServer.EndRemoveFromQueue, Driver.OnMusicServerError);
            yield return true; // wait for async reply
            if (!removeFromQueue.Success)
            {
                Error = "RemoveFromQueue failed";
                yield break;
            }

            Driver.SendMessageElements(SingleOriginator, delegate(MessageBuilder messageBuilder)
            {
                messageBuilder.Writer.WriteStartElement("Parcel");
                SerializeRecipientId(messageBuilder.Writer);
                messageBuilder.Writer.WriteStartElement("DoneRemoveFromQueue");

                foreach (TrackId trackId in trackIds)
                {
                    messageBuilder.Writer.WriteStartElement("RemovedTrackFromQueue");
                    messageBuilder.Writer.WriteAttributeString("AlbumId", trackId.AlbumId);
                    messageBuilder.Writer.WriteAttributeString("TrackNumber",
                        XmlConvert.ToString(trackId.TrackNumber));
                    messageBuilder.Writer.WriteEndElement(); // end "RemovedTrackFromQueue"
                }

                messageBuilder.Writer.WriteEndElement(); // end "DoneRemoveFromQueue"
                messageBuilder.Writer.WriteEndElement(); // end "Parcel"
            });
        }
	}

    class RemoveFromQueueMultipleAlbumsRequest : MusicPlayerRequest
    {
        public TrackId[] TrackIds;

        public RemoveFromQueueMultipleAlbumsRequest(MusicPlayerDriver driver, Guid originator,
                SyncNodeOperation operation) :
            base(driver, originator, operation)
        {
        }

        protected override IEnumerable<bool> Implementation()
        {
            Driver.Trace(true, "HW: Remove {0} track(s) from queue", TrackIds.Length);
            var removeFromQueue = AsyncResultHolder.Run(this, Driver.WorkQueue,
                (onDone) => Driver._musicServer.BeginRemoveFromQueue(TrackIds, onDone, null),
                Driver._musicServer.EndRemoveFromQueue, Driver.OnMusicServerError);
            yield return true; // wait for async reply
            if (!removeFromQueue.Success)
            {
                Error = "RemoveFromQueue failed";
                yield break;
            }
        }
	}

    class ChangeQueueRequest : MusicPlayerRequest
    {
        public TrackId[] TrackIds;

        public ChangeQueueRequest(MusicPlayerDriver driver, Guid originator,
                SyncNodeOperation operation) :
            base(driver, originator, operation)
        {
        }

        protected override IEnumerable<bool> Implementation()
        {
            Driver.Trace(true, "HW: Change queue to contain {0} track(s)", TrackIds.Length);
            var changeQueue = AsyncResultHolder.Run(this, Driver.WorkQueue,
                (onDone) => Driver._musicServer.BeginChangeQueue(TrackIds, onDone, null),
                Driver._musicServer.EndChangeQueue, Driver.OnMusicServerError);
            yield return true; // wait for async reply
            if (!changeQueue.Success)
            {
                Error = "ChangeQueue failed";
                yield break;
            }
        }
	}

    // clear the queue and play the specified album, starting at the specified track;
    // all tracks of the album are in the queue so that "|<" and ">|" navigate among all
    // tracks
    class PlayAlbumRequest : MusicPlayerRequest
    {
        public string AlbumId;
        public int StartAtTrackNumber;

        public PlayAlbumRequest(MusicPlayerDriver driver, Guid originator,
                SyncNodeOperation operation, Guid recipientId) :
            base(driver, originator, operation, recipientId)
        {
        }

        protected override IEnumerable<bool> Implementation()
        {
            Driver.Trace(true, "HW: Play album \"{0}\" from track {1}", AlbumId,
				StartAtTrackNumber);
            var playAlbum = AsyncResultHolder.Run(this, Driver.WorkQueue,
                (onDone) => Driver._musicServer.BeginPlayAlbum(AlbumId, StartAtTrackNumber,
					onDone, null), Driver._musicServer.EndPlayAlbum, Driver.OnMusicServerError);
            yield return true; // wait for async reply
            if (!playAlbum.Success)
            {
                Error = "PlayAlbum failed";
                yield break;
            }

            Driver.SendMessageElements(SingleOriginator, delegate(MessageBuilder messageBuilder)
            {
                messageBuilder.Writer.WriteStartElement("Parcel");
                SerializeRecipientId(messageBuilder.Writer);
                messageBuilder.Writer.WriteStartElement("DonePlayAlbum");
				messageBuilder.Writer.WriteAttributeString("AlbumId", AlbumId);
				messageBuilder.Writer.WriteAttributeString("StartAtTrackNumber",
					XmlConvert.ToString(StartAtTrackNumber));
                messageBuilder.Writer.WriteEndElement(); // end "DonePlayAlbum"
                messageBuilder.Writer.WriteEndElement(); // end "Parcel"
            });
        }
	}

	// start playing from the specified queue item
    class PlayQueueItemRequest : MusicPlayerRequest
    {
        public TrackId TrackId;

        public PlayQueueItemRequest(MusicPlayerDriver driver, Guid originator,
                SyncNodeOperation operation) :
            base(driver, originator, operation)
        {
        }

        protected override IEnumerable<bool> Implementation()
        {
            Driver.Trace(true, "HW: Play from queue item \"{0}\" track {1}", TrackId.AlbumId,
				TrackId.TrackNumber);
            var playAlbum = AsyncResultHolder.Run(this, Driver.WorkQueue,
                (onDone) => Driver._musicServer.BeginPlayQueueItem(TrackId, onDone, null),
                Driver._musicServer.EndPlayQueueItem, Driver.OnMusicServerError);
            yield return true; // wait for async reply
            if (!playAlbum.Success)
            {
                Error = "PlayQueueItem failed";
                yield break;
            }
        }
	}

    [DebuggerDisplay("{_requestName} Request")]
    class GenericRequest : MusicPlayerRequest
    {
        string _requestName;
        Action<AsyncCallback> _beginMethod;
        Action<IAsyncResult> _endMethod;

        public GenericRequest(MusicPlayerDriver driver, Guid originator,
            SyncNodeOperation operation, string requestName,
            Action<AsyncCallback> beginMethod, Action<IAsyncResult> endMethod) :
            base(driver, originator, operation)
        {
            _requestName = requestName;
            _beginMethod = beginMethod;
            _endMethod = endMethod;
        }

        protected override IEnumerable<bool> Implementation()
        {
            Driver.Trace(true, "HW: {0}", _requestName);
            var command = AsyncResultHolder.Run(this, Driver.WorkQueue, _beginMethod, _endMethod,
                Driver.OnMusicServerError);
            yield return true; // wait for async reply
            if (!command.Success)
            {
                Error = String.Format("{0} failed", _requestName);
                yield break;
            }
        }
    }

    void OnMusicServerLibraryChanged()
    {
        _libraryChangeCount++;
        BroadcastSyncNodeUpdate(SyncPath.Build(Name, "LibraryChangeCount"), _libraryChangeCount,
            NoOriginators);
    }

    void OnMusicServerQueueChanged(QueueDetails queueDetails)
    {
        // notify clients that the queue changed
        _queueChangeCount++;
        BroadcastSyncNodeUpdate(SyncPath.Build(Name, "QueueChangeCount"), _queueChangeCount,
            NoOriginators);

		if (_status != null)
		{
            // update <_status> with information about the queue change
            bool foundTrackInQueue = false;
            _status.QueuedTrackCount = 0;
            _status.QueuedTrackDuration = 0;
			for (int queueIndex = 0; queueIndex < queueDetails.Tracks.Length; queueIndex++)
			{
				AlbumTrackDetails trackInQueue = queueDetails.Tracks[queueIndex];
				if ((trackInQueue.AlbumId == _status.AlbumId) &&
                    (trackInQueue.TrackDetails.TrackNumber == _status.TrackNumber))
				{
					foundTrackInQueue = true;
				}
				else
				if (foundTrackInQueue)
				{
                    _status.QueuedTrackCount++;
                    _status.QueuedTrackDuration += trackInQueue.TrackDetails.Duration;
				}
			}

            // notify clients of the change in QueueSummary (i.e. number and total duration of
            // tracks that are queued to be played after the current one)
            BroadcastMessageElements(delegate(Guid sessionId, MessageBuilder messageBuilder)
            {
                SerializeQueueSummary(messageBuilder.Writer);
            });
		}
    }

    void OnMusicServerStatusChanged(MusicServiceStatus status)
    {
        // ignore <status> if we haven't received detailed status yet
        if (_status == null)
            return;

        // update <_status> with <status>
        _status.PlayState = status.PlayState;
        _status.AlbumId = status.AlbumId;
        _status.TrackNumber = status.TrackNumber;
        _status.Position = status.Position;
        _status.QueueIndex = status.QueueIndex;

        // notifiy clients of the change in status
        BroadcastMusicServiceStatus();
    }

    void OnMusicServerSettingsChanged(MusicServiceSettings settings)
    {
        // ignore <settings> if we haven't received detailed status yet
        if (_status == null)
            return;
        
        // update <_status> with <settings>
        _status.Settings = settings;

        // notifiy clients of the change in settings
        BroadcastMessageElements(delegate(Guid sessionId, MessageBuilder messageBuilder)
        {
            SerializeSettingsSummary(messageBuilder.Writer);
        });
    }

    // IMusicWcfServiceCallback implementation...

    void IMusicWcfServiceCallback.StatusChanged(MusicServiceStatus status)
    {
        // warning: this method is called on a different thread!
        WorkQueue.Add(delegate
        {
            OnMusicServerStatusChanged(status);
        });
    }

    void IMusicWcfServiceCallback.QueueChanged(QueueDetails queueDetails)
    {
        // warning: this method is called on a different thread!
        WorkQueue.Add(delegate
        {
            OnMusicServerQueueChanged(queueDetails);
        });
    }

    void IMusicWcfServiceCallback.LibraryChanged()
    {
        // warning: this method is called on a different thread!
        WorkQueue.Add(OnMusicServerLibraryChanged);
    }

    void IMusicWcfServiceCallback.SettingsChanged(MusicServiceSettings settings)
    {
        // warning: this method is called on a different thread!
        WorkQueue.Add(delegate
        {
            OnMusicServerSettingsChanged(settings);
        });
    }

#if !SILVERLIGHT

    // async callbacks are not used...

    IAsyncResult IMusicWcfServiceCallback.BeginStatusChanged(MusicServiceStatus status, AsyncCallback callback, object asyncState)
    {
        throw new NotImplementedException();
    }

    void IMusicWcfServiceCallback.EndStatusChanged(IAsyncResult result)
    {
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfServiceCallback.BeginQueueChanged(QueueDetails queueDetails, AsyncCallback callback, object asyncState)
    {
        throw new NotImplementedException();
    }

    void IMusicWcfServiceCallback.EndQueueChanged(IAsyncResult result)
    {
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfServiceCallback.BeginLibraryChanged(AsyncCallback callback, object asyncState)
    {
        throw new NotImplementedException();
    }

    void IMusicWcfServiceCallback.EndLibraryChanged(IAsyncResult result)
    {
        throw new NotImplementedException();
    }

    IAsyncResult IMusicWcfServiceCallback.BeginSettingsChanged(MusicServiceSettings settings, AsyncCallback callback, object asyncState)
    {
        throw new NotImplementedException();
    }

    void IMusicWcfServiceCallback.EndSettingsChanged(IAsyncResult result)
    {
        throw new NotImplementedException();
    }

#endif

#if false
    class QueueSummary
    {
        /// <summary>
        /// The number of tracks queued to be played after the current track.
        /// </summary>
        public int FutureQueuedTrackCount;

        /// <summary>
        /// The sum of the duration of the tracks queued to be played after the current track,
        /// in seconds.
        /// </summary>
        public double FutureQueuedTrackDuration;
    }
#endif

    enum QueueSaveState
    {
        /// <summary>
        /// The queue is new (i.e. not associated with a playlist).
        /// </summary>
        New,

        /// <summary>
        /// Queue was loaded from playlist <s>MusicPlayerDriver._queuePlaylistName</s>, and the queue
        /// has not been modified since then.
        /// </summary>
        Loaded,

        /// <summary>
        /// The queue was loaded from playlist <s>MusicPlayerDriver._queuePlaylistName</s>, and the queue
        /// has been modified since then.
        /// </summary>
        LoadedModified,

        /// <summary>
        /// The queue was was saved to playlist <s>MusicPlayerDriver._queuePlaylistName</s>, and the queue
        /// has not been modified since then.
        /// </summary>
        Saved,

        /// <summary>
        /// The queue was was saved to playlist <s>MusicPlayerDriver._queuePlaylistName</s>, and the queue
        /// has been modified since then.
        /// </summary>
        SavedModified,
    }
}

}
